/*file chiron src/jsonp.js */
/*preamble

    Copyright (c) 2002-2008 Kris Kowal <http://cixar.com/~kris.kowal>
    MIT License
    
    The license terms are stated in full in <license.rst> and at the end
    of all source files.

*/

/*status incomplete */

/**
    provides a solution for cross-browser, cross-platform, cross-domain,
    cross-site JSONP (JSON with Padding) script requests with an interface
    similar to HTTP requests.


    Security
    ========

    If you're using a cross-site script, you are only as secure
    as your trust in the remote script, the intervening
    DNS servers, your SSL certificate authority, and any number
    of intervening parties.  However, security is very different
    than safety.


    Safety
    ======

    To prevent collisions and to provide stop-gaps against
    malicious or foolish remote scripts, this module takes
    some precautions.  Since you're using modules.js already,
    you have a free assurance, if not guarantee, that your
    modules' name spaces are inaccessible from the global
    object.  Therefore data therein is moderately protected
    from accidental tampering.  This is a great deal
    safer than global namespace objects, but does not protect
    against coders who drop their payload in global objects
    like the Array and Object constructors or window.name.

    This module needs to communicate with outside
    world scripts, and the only way to do that is through a
    global callback function.  flash.js does something similar
    to register new Flash movie script bridges.  To
    help avoid global namespace collisions, jsonp.js
    only exposes its __jsonp__respond global function
    when a request is pending a response.  Furthermore,
    the module serializes requests so only one response
    will be pending at any given time.  __jsonp__respond
    removes itself from the global namespace whenever
    new data arrives.

*/

include('base.js');
include('http.js');
include('browser.js');
include('event.js');
include('optioned.js');

warn('This page cannot protect confidential data  because it uses JSONP.');

/*** request
    does not support blocking.
*/
exports.request = foreignModuleBind(function (url, okObserver) {
    queue.push(Request(url, okObserver));
    next();
});

/*** logging
    whether to log requests.
*/
exports.logging = false;

/*** Request
*/
exports.Request = type([Optioned, Observable], function (self, supr) {

    var url;
    var callbackKey = 'callback';
    var callbackValue = '__jsonp__respond' + callbackNumbers.next();
    var cache = true;
    var query;
    var timeout = 5000;
    var timeoutHandle;

    /**** init
    */
    self.init = function (options, okObserver) {
        if (isInstance(options, String))
            options = {'url': options};
        supr.init(options);
        if (okObserver) {
            self.setOption('ok', okObserver);
        }
    };

    /**** setOption
    */
    self.setOption = function (key, value) {
        if (key == 'url') {
            var urlObject = parseUrl(value);
            query = urlObject.query;
            urlObject.query = '';
            url = formatUrl(urlObject);
        } else if (key == 'ok') {
            self.observe('ok', value);
        } else if (key == 'error') {
            self.observe('error', value);
        } else if (key == 'ready') {
            self.observe('ready', value);
        } else if (key == 'query') {
            query = value;
        } else if (key == 'cache') {
            cache = value;
        } else if (key == 'callbackKey') {
            callbackKey = value;
        } else if (key == 'timeout') {
            timeout = value;
        } else {
            throw new Error(
                key + ' is not a valid option for ' + 
                self.getTypeName()
            );
        }
    };

    /**** send
    */
    self.send = function () {
        window[callbackValue] = respond;
        script = document.createElement('script');
        var src = url + '?';
        if (query) {
            src += query + '&';
        }
        src += callbackKey + '=' + callbackValue;
        if (!cache) {
            src += '#' + (new Date().getTime())
        }
        if (logging)
            log(src, 'load');
        script.src = src;
        /* safari would block here for same-domain requests */

        if (isInstance(timeout, Number)) {
            timeoutHandle = setTimeout(function () {
                self.error(repr(url) + ' timed out after ' + timeout + 'ms');
                self.abort();
            }, timeout);
        }

        schedule(function () {
            head.appendChild(script);
        });

    };

    /**** abort
    */
    self.abort = function () {
        if (!no(timeoutHandle))
            clearTimeout(timeoutHandle);
        window[callbackValue] = function () {
            delete window[callbackValue];
        };
        abort();
    };

    /**** ready
    */
    self.ready = State(Event, function (value, error) {
        if (!no(timeoutHandle))
            clearTimeout(timeoutHandle);
        head.removeChild(script);

        window[callbackValue] = undefined;
        // IE can't handle deleting from window
        try {
            delete window[callbackValue];
        } catch (exception) { }

        if (error !== undefined) {
            self.error(error);
        } else {
            self.ok(value);
        }
    });

    /**** ok
    */
    self.ok = State();

    /**** error
    */
    self.error = State(Event, function (message) {
        error(message);
    });

});


/* at most one Request that is waiting for a script to call back with
 * its response */
var pending;
var queue = [];
var head;
var script;
var requestNumber = range().iter();
var callbackNumbers = range().iter();

head = document.getElementsByTagName('head')[0] || document.documentElement;

var next = function () {
    if (!pending && queue.length) {
        pending = queue.shift();
        pending.send();
    }
};

var respond = function (value, error) {
    try {
        try {
            pending.ready(value, error);
        } catch (exception) {
            error(repr(exception));
        }
    } finally {
        /* if there was an error, make sure it
         * gets reported before we move on */
        schedule(function () {
            pending = undefined;
            next();
        });
    }
};

var abort = function () {
    pending = undefined;
    next();
};


/*license

    Legal
    =======
    
    Chiron is a component of the Tale web-game project.
    
    See <credit.txt> for a complete list of
    contributions and their licenses.  All contributions are provided
    under permissive, non-viral licenses including MIT, BSD, Creative Commons
    Attribution 2.5, Public Domain, or Unrestricted.
    
    
    License
    =======
    
    Copyright (c) 2002-2008 Kris Kowal <http://cixar.com/~kris.kowal>
    MIT License
    
    
    MIT License
    -----------
    
    Permission is hereby granted, free of charge, to any person
    obtaining a copy of this software and associated documentation
    files (the "Software"), to deal in the Software without
    restriction, including without limitation the rights to use,
    copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following
    conditions:
    
    The above copyright notice and this permission notice shall be
    included in all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    OTHER DEALINGS IN THE SOFTWARE.

*/

